/// <reference path='Scripts/typings/knockout/knockout.d.ts'/>
module DMath {
	export class Letter {
		public letter: KnockoutObservable<string>;
		constructor(letter: string) {
			this.letter = ko.observable(letter);
		}
	}
	export class State {
		public name: KnockoutObservable<string>;
		public isFinal: KnockoutComputed<boolean>;
		public isStart: KnockoutComputed<boolean>


		constructor(name: string) {
			this.name = ko.observable(name);
		}

	}
	export class Configuration {
		public state: KnockoutObservable<string>;
		public letter: KnockoutObservable<string>;

		constructor(state: string, letter: string) {
			this.state = ko.observable(state);
			this.letter = ko.observable(letter);
		}
	}

	export class Transition {
		public state: State;
		public transitions: KnockoutObservableArray<KnockoutObservableArray<State>>;

		constructor(state: State) {
			this.state = state;
			this.transitions = ko.observableArray<KnockoutObservableArray<State>>();
		}
	}

	export class AutomatonViewModel {
		
		private static defaultAutomatonObject = {
			alphabet: ['a'],
			states: [{ name: 'default' }],
			transitions: [],
			finalStates: [],
			startState: { name: 'default' },
		};

		public addLetter: (letter: string) => void;
		public addState: (state: string, nextStates: KnockoutObservableArray) => void;
		public addStateTransition: (state: string, letterIndex: number) => void;
		public determine: () => void;
		public minimize: () => void;

		public name: string;
		public alphabet: KnockoutObservableArray<Letter>;
		public states: KnockoutObservableArray<State>;
		public transitions: KnockoutObservableArray<Transition>;
		public startState: State;
		public finalStates: KnockoutObservableArray<State>;
		public visualizing: KnockoutObservable<boolean>;

		public sigma: any;

		constructor(obj) {
			obj = obj || AutomatonViewModel.defaultAutomatonObject;

			this.name = "unnamed";
			this.alphabet = ko.observableArray<Letter>();
			this.states = ko.observableArray<State>();
			this.finalStates = ko.observableArray<State>();
			this.transitions = ko.observableArray<Transition>();

			this.update(obj);
			this.visualizing = ko.observable(false);

			this.addLetter = () => {
				var previousLetterCode: number = this.alphabet()[this.alphabet().length - 1].letter.charCodeAt(0);
				var letter: string = String.fromCharCode(previousLetterCode + 1);
				this.alphabet.push(new Letter(letter));
				for (var i = 0; i < this.transitions().length; i++) {
					var currentTransition: Transition = this.transitions()[i];
					currentTransition.transitions.push(ko.observableArray<State>());
				}
			}

			this.addStateTransition = (state: string, letterIndex: number) => {
				// Find the transitions going out of the state
				var currentTransition: Transition = ko.utils.arrayFirst(this.transitions(), (transition) => transition.state.name() == state);
				// Select the transitions going out of the state with the given letter
				var transitionFromStateWithLetter: KnockoutObservableArray<State> = currentTransition.transitions()[letterIndex];
				// Push the first state as default
				transitionFromStateWithLetter.push(this.states()[0]);
			}

			this.addState = () => {
				var stateName: string = "new.state" + Math.random() * 100;

				var state: State = new State(stateName);
				AutomatonViewModel.attachStartFinalComputed(this, state);
				this.states.push(state);
				var transition: Transition = new Transition(state);
				for (var k = 0; k < this.alphabet().length; k++) {
					var nextStates: KnockoutObservableArray<State> = ko.observableArray<State>([]);
					transition.transitions.push(nextStates);
				}
				this.transitions.push(transition);
			}
		}



		private static attachStartFinalComputed(self: AutomatonViewModel, state: State): void {
			state.isFinal = ko.computed({
				read: function (): boolean {
					return ko.utils.arrayIndexOf(<any[]>self.finalStates(), <any>state) != -1;
				},
			});
			state.isStart = ko.computed({
				read: function () {
					return self.startState.name() == state.name();
				},
			});
		}

		public update(obj): void {
			this.alphabet.removeAll();
			obj.alphabet.sort();
			for (var i = 0; i < obj.alphabet.length; i++) {
				this.alphabet.push(new Letter(obj.alphabet[i]));
			}

			this.states.removeAll();
			this.finalStates.removeAll();

			for (var i = 0; i < obj.states.length; i++) {
				var state = new State(obj.states[i].name);
				this.states.push(state);
				for (var j = 0; j < obj.finalStates.length; j++) {
					if (obj.finalStates[j].name == state.name()) {
						this.finalStates.push(state);
					}
				}
			}
			this.startState = ko.utils.arrayFirst(this.states(), (x) => { return x.name() == obj.startState.name });

			var sortedKeys: Array<string> = Object.keys(obj.transitions);
			sortedKeys.sort();

			this.transitions.removeAll();
			for (var j = 0; j < this.states().length; j++) {
				var state: State = this.states()[j];

				var transition: Transition = new Transition(state);
				for (var k = 0; k < this.alphabet().length; k++) {
					var letter: Letter = this.alphabet()[k];
					var key: string = "State: " + state.name() + ", Letter: " + letter.letter();

					var table = obj.transitions[key] || [];
					// Filter the elements in this.states, whose names are contained somewhere in the table
					var filter = (x) => table.filter((y) => y.name == x.name()).length != 0; // TODO: This is slow
					var nextStates: KnockoutObservableArray<State> = ko.observableArray<State>(ko.utils.arrayFilter(<any[]>this.states(), filter));
					transition.transitions.push(nextStates);
				}
				this.transitions.push(transition);
			}

			for (var i = 0; i < this.states().length; i++) {
				var state: State = this.states()[i];
				AutomatonViewModel.attachStartFinalComputed(this, state);
			}

			if (this.sigma) {
				this.buildGraph();
				this.sigma.draw();
			}
		}

		public initSigma(sigRoot) {
			var sig = sigma.init(sigRoot);
			sig.drawingProperties({
				defaultLabelColor: '#ccc',
				font: 'Arial',
				edgeColor: 'target',
				defaultEdgeType: 'curve',
			}).graphProperties({
				minNodeSize: 1,
				maxNodeSize: 10
			});

			this.sigma = sig;
			this.buildGraph();
			this.sigma.draw();
		}

		// Convert to json to send to the server

		public toJson(): any {
			var obj: any = {};
			var ignoredProperties = {
				"ignore": ["isFinal", "isStart"]
			};
			obj.states = ko.mapping.toJS(this.states, ignoredProperties);
			obj.alphabet = [];
			for (var i = 0; i < this.alphabet().length; i++) {
				obj.alphabet.push(this.alphabet()[i].letter());
			}

			obj.transitions = {};
			for (var i = 0; i < this.transitions().length; i++) {
				var currentTransition: Transition = this.transitions()[i];
				var state: State = currentTransition.state;

				for (var j = 0; j < currentTransition.transitions().length; j++) {
					var letter: Letter = this.alphabet()[j];
					var nextStates: Array<State> = ko.mapping.toJS(currentTransition.transitions()[j], ignoredProperties);
					if (nextStates.length != 0) {
						var key: string = "State: " + state.name() + ", Letter: " + letter.letter();
						obj.transitions[key] = nextStates;
					}
				}
			}
			obj.startState = ko.mapping.toJS(this.startState, ignoredProperties);
			obj.finalStates = ko.mapping.toJS(this.finalStates, ignoredProperties);
			return JSON.stringify(obj);
		}

		// Graph building

		private getNodeColor(state: State): string {
			var color: string = "green";
			if (state.isStart() && state.isFinal()) {
				color = "purple";
			}
			else if (state.isStart()) {
				color = "blue";
			}
			else if (state.isFinal()) {
				color = "red";
			}
			return color;
		}

		private buildNodes(): void {
			//var radius = 20;
			//var center = { x: radius / 2, y: radius / 2 };
			//for (var i = 0; i < this.states().length; i++) {
			//	var state: State = this.states()[i];
			//	var obj: any = {
			//		label: state.name(),
			//		color: this.getNodeColor(state),
			//		x: center.x + radius * Math.cos(2 * Math.PI * i / this.states().length),
			//		y: center.y + radius * Math.sin(2 * Math.PI * i / this.states().length)

			//	}
			//	this.sigma.addNode(state.name(), obj);
			//}

			var maxWidth = 1;
			var maxHeight = 1;

			// Split all states but the first in triples, draw the triples in a column.
			var triples = (this.states().length - 1) / 3;
			var triplesEnumerated = 0;
			var step = maxWidth / triples;

			var state: State = this.states()[0];
			var obj: any = {
				label: state.name(),
				color: this.getNodeColor(state),
				x: step,
				y: maxHeight / 2,
			}
			this.sigma.addNode(state.name(), obj);

			for (var i = 1; i < this.states().length; i++) {
				var state: State = this.states()[i];
				var obj: any = {
					label: state.name(),
					color: this.getNodeColor(state),
					x: (triplesEnumerated + 2) * step,
					y: (i % 3) * maxHeight / 3
				}
				if (i % 3 == 0) {
					triplesEnumerated++;
				}
				this.sigma.addNode(state.name(), obj);
			}


		}

		private buildEdges(): void {
			for (var stateTransitions = 0; stateTransitions < this.transitions().length; stateTransitions++) {
				var transitions: Transition = this.transitions()[stateTransitions];
				for (var i = 0; i < transitions.transitions().length; i++) {
					var letter: Letter = this.alphabet()[i];

					for (var j = 0; j < transitions.transitions()[i]().length; j++) {
						var next: State = transitions.transitions()[i]()[j];
						var name: string = next.name();
						var edgeName: string = transitions.state.name() + name;
						var edge;
						try {
							// Test if the edge is already in the graph
							var edge = this.sigma.getEdges(edgeName);
						}
						catch (e) {
							edge = false;
						}
						var label = letter.letter();
						if (edge) {
							// If it is in the graph, remove it, change the label, add it again.
							label = edge.label + ", " + letter.letter();
							this.sigma.dropEdge(edgeName);
						}
						this.sigma.addEdge(edgeName, transitions.state.name(), name, { label: label, arrow: 'target', arrowDisplaySize: 100 });
					}
				}
			}
		}

		private buildGraph(): void {
			this.sigma.emptyGraph();
			var startState: State = this.startState;

			this.buildNodes();			

			this.buildEdges();
		}

		public toggleVisualization(sigRoot): void {
			if (!this.visualizing()) {
				this.buildGraph();
				this.sigma.draw();
			}
			this.visualizing(!this.visualizing());
		}
	}
}